Chapter 21

Project: Data Fetch System using Free Online API

Session 21

Learning Objectives

By the end of this chapter, you will be able to:

1

Project Overview

Project Goal

Build a "Users Directory" app that fetches user data from JSONPlaceholder API (https://jsonplaceholder.typicode.com/users), displays it in a list, and allows users to view details and search.

Features to Implement

  • Fetch users from API on app launch
  • Display users in a scrollable list
  • Show user details in a detail screen
  • Pull-to-refresh functionality
  • Search/filter users by name
  • Loading and error states
2

Project Setup

Step 1: Create Project

flutter create users_directory
cd users_directory

Step 2: Add Dependencies

Update pubspec.yaml:

pubspec.yaml

name: users_directory
description: A Flutter app that fetches and displays users from API
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  cupertino_icons: ^1.0.6

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

Step 3: Install Dependencies

flutter pub get
3

Project Structure

Suggested Folder Layout

lib/
  main.dart
  models/
    user.dart
    address.dart
    company.dart
  services/
    api_service.dart
  screens/
    home_screen.dart
    user_detail_screen.dart
  widgets/
    user_card.dart
    loading_widget.dart
    error_widget.dart
  utils/
    constants.dart
4

Creating Data Models

User Model

Create lib/models/user.dart:

lib/models/user.dart

class User {
  final int id;
  final String name;
  final String username;
  final String email;
  final String phone;
  final String website;
  final Address address;
  final Company company;

  User({
    required this.id,
    required this.name,
    required this.username,
    required this.email,
    required this.phone,
    required this.website,
    required this.address,
    required this.company,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      username: json['username'] as String,
      email: json['email'] as String,
      phone: json['phone'] as String,
      website: json['website'] as String,
      address: Address.fromJson(json['address'] as Map),
      company: Company.fromJson(json['company'] as Map),
    );
  }

  Map toJson() {
    return {
      'id': id,
      'name': name,
      'username': username,
      'email': email,
      'phone': phone,
      'website': website,
      'address': address.toJson(),
      'company': company.toJson(),
    };
  }
}

class Address {
  final String street;
  final String suite;
  final String city;
  final String zipcode;
  final Geo geo;

  Address({
    required this.street,
    required this.suite,
    required this.city,
    required this.zipcode,
    required this.geo,
  });

  factory Address.fromJson(Map json) {
    return Address(
      street: json['street'] as String,
      suite: json['suite'] as String,
      city: json['city'] as String,
      zipcode: json['zipcode'] as String,
      geo: Geo.fromJson(json['geo'] as Map),
    );
  }

  Map toJson() {
    return {
      'street': street,
      'suite': suite,
      'city': city,
      'zipcode': zipcode,
      'geo': geo.toJson(),
    };
  }

  String get fullAddress => '$street, $suite, $city $zipcode';
}

class Geo {
  final String lat;
  final String lng;

  Geo({required this.lat, required this.lng});

  factory Geo.fromJson(Map json) {
    return Geo(
      lat: json['lat'] as String,
      lng: json['lng'] as String,
    );
  }

  Map toJson() {
    return {
      'lat': lat,
      'lng': lng,
    };
  }
}

class Company {
  final String name;
  final String catchPhrase;
  final String bs;

  Company({
    required this.name,
    required this.catchPhrase,
    required this.bs,
  });

  factory Company.fromJson(Map json) {
    return Company(
      name: json['name'] as String,
      catchPhrase: json['catchPhrase'] as String,
      bs: json['bs'] as String,
    );
  }

  Map toJson() {
    return {
      'name': name,
      'catchPhrase': catchPhrase,
      'bs': bs,
    };
  }
}
5

Creating API Service

API Service Class

Create lib/services/api_service.dart:

lib/services/api_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/user.dart';

class ApiService {
  static const String baseUrl = 'https://jsonplaceholder.typicode.com';

  Future> fetchUsers() async {
    try {
      final response = await http.get(
        Uri.parse('$baseUrl/users'),
      ).timeout(Duration(seconds: 10));

      if (response.statusCode == 200) {
        final List jsonList = jsonDecode(response.body);
        return jsonList.map((json) => User.fromJson(json)).toList();
      } else {
        throw Exception('Failed to load users: ${response.statusCode}');
      }
    } catch (e) {
      throw Exception('Error fetching users: $e');
    }
  }

  Future fetchUserById(int id) async {
    try {
      final response = await http.get(
        Uri.parse('$baseUrl/users/$id'),
      ).timeout(Duration(seconds: 10));

      if (response.statusCode == 200) {
        final json = jsonDecode(response.body);
        return User.fromJson(json);
      } else {
        throw Exception('Failed to load user: ${response.statusCode}');
      }
    } catch (e) {
      throw Exception('Error fetching user: $e');
    }
  }
}
6

Creating Constants

lib/utils/constants.dart

class AppConstants {
  static const String appName = 'Users Directory';
  static const String apiBaseUrl = 'https://jsonplaceholder.typicode.com';
}
7

Creating Reusable Widgets

Loading Widget

Create lib/widgets/loading_widget.dart:

lib/widgets/loading_widget.dart

import 'package:flutter/material.dart';

class LoadingWidget extends StatelessWidget {
  const LoadingWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CircularProgressIndicator(),
          SizedBox(height: 16),
          Text(
            'Loading users...',
            style: TextStyle(fontSize: 16),
          ),
        ],
      ),
    );
  }
}

Error Widget

Create lib/widgets/error_widget.dart:

lib/widgets/error_widget.dart

import 'package:flutter/material.dart';

class ErrorDisplayWidget extends StatelessWidget {
  final String message;
  final VoidCallback? onRetry;

  const ErrorDisplayWidget({
    Key? key,
    required this.message,
    this.onRetry,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Colors.red,
            ),
            SizedBox(height: 16),
            Text(
              'Error',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(height: 8),
            Text(
              message,
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 14),
            ),
            if (onRetry != null) ...[
              SizedBox(height: 24),
              ElevatedButton.icon(
                onPressed: onRetry,
                icon: Icon(Icons.refresh),
                label: Text('Retry'),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

User Card Widget

Create lib/widgets/user_card.dart:

lib/widgets/user_card.dart

import 'package:flutter/material.dart';
import '../models/user.dart';

class UserCard extends StatelessWidget {
  final User user;
  final VoidCallback onTap;

  const UserCard({
    Key? key,
    required this.user,
    required this.onTap,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Theme.of(context).primaryColor,
          child: Text(
            user.name[0].toUpperCase(),
            style: TextStyle(color: Colors.white),
          ),
        ),
        title: Text(
          user.name,
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(height: 4),
            Text(user.email),
            Text(user.phone),
          ],
        ),
        trailing: Icon(Icons.chevron_right),
        onTap: onTap,
      ),
    );
  }
}
8

Creating Home Screen

lib/screens/home_screen.dart

import 'package:flutter/material.dart';
import '../models/user.dart';
import '../services/api_service.dart';
import '../widgets/user_card.dart';
import '../widgets/loading_widget.dart';
import '../widgets/error_widget.dart';
import 'user_detail_screen.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State {
  final ApiService _apiService = ApiService();
  List _users = [];
  List _filteredUsers = [];
  bool _isLoading = false;
  String? _error;
  final TextEditingController _searchController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _loadUsers();
    _searchController.addListener(_filterUsers);
  }

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  Future _loadUsers() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final users = await _apiService.fetchUsers();
      setState(() {
        _users = users;
        _filteredUsers = users;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  void _filterUsers() {
    final query = _searchController.text.toLowerCase();
    setState(() {
      if (query.isEmpty) {
        _filteredUsers = _users;
      } else {
        _filteredUsers = _users.where((user) {
          return user.name.toLowerCase().contains(query) ||
                 user.email.toLowerCase().contains(query);
        }).toList();
      }
    });
  }

  void _navigateToDetail(User user) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => UserDetailScreen(user: user),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Users Directory'),
        actions: [
          if (!_isLoading && _error == null)
            IconButton(
              icon: Icon(Icons.refresh),
              onPressed: _loadUsers,
              tooltip: 'Refresh',
            ),
        ],
      ),
      body: Column(
        children: [
          // Search bar
          if (!_isLoading && _error == null)
            Padding(
              padding: EdgeInsets.all(16),
              child: TextField(
                controller: _searchController,
                decoration: InputDecoration(
                  hintText: 'Search users by name or email...',
                  prefixIcon: Icon(Icons.search),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
            ),
          // Content
          Expanded(
            child: _buildBody(),
          ),
        ],
      ),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return LoadingWidget();
    }

    if (_error != null) {
      return ErrorDisplayWidget(
        message: _error!,
        onRetry: _loadUsers,
      );
    }

    if (_filteredUsers.isEmpty) {
      return Center(
        child: Text(
          _searchController.text.isEmpty
              ? 'No users found'
              : 'No users match your search',
          style: TextStyle(fontSize: 16),
        ),
      );
    }

    return RefreshIndicator(
      onRefresh: _loadUsers,
      child: ListView.builder(
        itemCount: _filteredUsers.length,
        itemBuilder: (context, index) {
          final user = _filteredUsers[index];
          return UserCard(
            user: user,
            onTap: () => _navigateToDetail(user),
          );
        },
      ),
    );
  }
}
9

Creating User Detail Screen

lib/screens/user_detail_screen.dart

import 'package:flutter/material.dart';
import '../models/user.dart';

class UserDetailScreen extends StatelessWidget {
  final User user;

  const UserDetailScreen({Key? key, required this.user}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(user.name),
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildSection(
              'Personal Information',
              [
                _buildInfoRow('Name', user.name),
                _buildInfoRow('Username', user.username),
                _buildInfoRow('Email', user.email),
                _buildInfoRow('Phone', user.phone),
                _buildInfoRow('Website', user.website),
              ],
            ),
            SizedBox(height: 24),
            _buildSection(
              'Address',
              [
                _buildInfoRow('Street', user.address.street),
                _buildInfoRow('Suite', user.address.suite),
                _buildInfoRow('City', user.address.city),
                _buildInfoRow('Zipcode', user.address.zipcode),
                _buildInfoRow('Full Address', user.address.fullAddress),
                _buildInfoRow('Coordinates', '${user.address.geo.lat}, ${user.address.geo.lng}'),
              ],
            ),
            SizedBox(height: 24),
            _buildSection(
              'Company',
              [
                _buildInfoRow('Name', user.company.name),
                _buildInfoRow('Catch Phrase', user.company.catchPhrase),
                _buildInfoRow('Business', user.company.bs),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSection(String title, List children) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          title,
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
          ),
        ),
        SizedBox(height: 12),
        Card(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              children: children,
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 120,
            child: Text(
              '$label:',
              style: TextStyle(
                fontWeight: FontWeight.w600,
                color: Colors.grey[700],
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: TextStyle(fontSize: 14),
            ),
          ),
        ],
      ),
    );
  }
}
10

Updating Main.dart

lib/main.dart

import 'package:flutter/material.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Users Directory',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: HomeScreen(),
    );
  }
}
11

Running the App

Commands

# Run the app
flutter run

# Run in release mode
flutter run --release

# Build APK
flutter build apk
12

Project Checklist

Implementation Checklist

  • ✅ Project created and dependencies added
  • ✅ Folder structure organized
  • ✅ User model with nested Address and Company models
  • ✅ API service with error handling
  • ✅ Loading widget for async states
  • ✅ Error widget with retry functionality
  • ✅ User card widget for list items
  • ✅ Home screen with search and pull-to-refresh
  • ✅ User detail screen with complete information
  • ✅ Main.dart configured
13

Enhancement Ideas

Optional Enhancements

  • Add local caching using SharedPreferences
  • Implement pagination for large datasets
  • Add favorite users functionality
  • Implement dark mode support
  • Add user avatar images
  • Create a map view showing user locations
  • Add sorting options (by name, email, etc.)
14

Exercises

1. Complete Implementation

Implement the entire project following the structure and code provided. Test all features including search, pull-to-refresh, and navigation to detail screen.

2. Add Caching

Implement local caching using SharedPreferences. Save fetched users locally and load them on app start while fetching fresh data in the background.

3. Add Favorites

Add a favorite button to each user card. Store favorite user IDs locally and show a separate favorites screen with only favorited users.